package crmdna.member;

import static crmdna.common.OfyService.ofy;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.appengine.api.NamespaceManager;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Work;
import com.googlecode.objectify.cmd.Query;

import crmdna.common.APIException;
import crmdna.common.APIResponse.Status;
import crmdna.common.Utils;
import crmdna.contactdetail.ContactDetail;
import crmdna.contactdetail.ContactDetailProp;
import crmdna.program.Program;
import crmdna.program.ProgramProp;
import crmdna.programtype.ProgramType;
import crmdna.sequence.Sequence;
import crmdna.sequence.Sequence.SequenceType;

public class MemberCore {
	public static class MemberProp implements Comparable<MemberProp> {
		public long memberId;
		public ContactDetailProp contact;
		public String name;

		// public Set<String> qsTags = new HashSet<>();
		public Set<Long> groupIds = new HashSet<Long>();
		public Set<Long> programIds = new HashSet<Long>();
		public Set<Long> programTypeIds = new HashSet<Long>();
		public Set<Long> practiceIds = new HashSet<Long>();

		public List<UnverifiedProgramProp> unverifiedProgramProps = new ArrayList<>();

		@Override
		public int compareTo(MemberProp o) {
			return this.name.compareTo(o.name);
		}
	}

	static MemberProp create(String namespace, long groupId,
			ContactDetailProp contact, boolean allowDuplicateEmail) {

		ContactDetail.ensureEmailOrPhoneNumberValid(contact);

		if (!allowDuplicateEmail) {
			// if email is specified - it should be unique
			if (null != contact.email) {
				contact.email = contact.email.toLowerCase();
				ensureEmailNotPresentInDB(namespace, contact.email);
				safeAddToMemcache(namespace, contact.email);
			}
		}

		MemberEntity memberEntity = new MemberEntity();
		memberEntity.memberId = Sequence
				.getNext(namespace, SequenceType.MEMBER);
		memberEntity.groupIds.add(groupId);

		memberEntity = populateContactDetails(memberEntity, contact);
		memberEntity = populateDependantFieldsWithoutDBQuery(memberEntity);

		ofy(namespace).save().entity(memberEntity).now();

		return memberEntity.toProp();
	}

	static void safeAddToMemcache(String namespace, String email) {
		// automatically add key to memcache, if already found throw exception

		String key = getUniqueKey(namespace, email.toLowerCase());
		NamespaceManager.set(namespace);
		long val = MemcacheServiceFactory.getMemcacheService().increment(key,
				1l, 0l);
		// note: call to increment is atomic
		if (val != 1)
			throw new APIException(Status.ERROR_RESOURCE_ALREADY_EXISTS,
					"There is already a member with email [" + email + "]");
	}

	private static String getUniqueKey(String namespace, String name) {
		return namespace + "_" + SequenceType.MEMBER + "_" + name;
	}

	static void ensureEmailNotPresentInDB(String namespace, String email) {

		email = email.toLowerCase();
		List<Key<MemberEntity>> keys = ofy(namespace).load()
				.type(MemberEntity.class).filter("email", email).keys().list();

		if (keys.size() != 0)
			throw new APIException(Status.ERROR_RESOURCE_ALREADY_EXISTS,
					"There is already a member with email [" + email + "]");
	}

	static MemberProp updateContactDetails(final String namespace,
			final long memberId, final ContactDetailProp contact) {

		MemberEntity memberEntity = safeGet(namespace, memberId);

		// if email is different it should not exist in a different entity
		if ((memberEntity.email != null)
				&& (contact.email != null)
				&& (!memberEntity.email.toLowerCase().equals(
						contact.email.toLowerCase()))) {
			ensureEmailNotPresentInDB(namespace, contact.email);
			safeAddToMemcache(namespace, contact.email);
		}

		MemberProp memberProp = ofy(namespace).transact(new Work<MemberProp>() {

			@Override
			public MemberProp run() {
				MemberEntity memberEntity = safeGet(namespace, memberId);
				memberEntity = populateContactDetails(memberEntity, contact);
				populateDependantFields(namespace, memberEntity);
				ofy(namespace).save().entity(memberEntity).now();

				return memberEntity.toProp();
			}
		});

		return memberProp;
	}

	static MemberProp addOrDeleteGroup(final String namespace,
			final long memberId, final long groupId, final boolean add) {

		MemberProp memberProp = ofy(namespace).transact(new Work<MemberProp>() {
			@Override
			public MemberProp run() {
				MemberEntity memberEntity = safeGet(namespace, memberId);

				boolean change;
				if (add) {
					change = memberEntity.groupIds.add(groupId);
				} else {
					change = memberEntity.groupIds.remove(groupId);
					if (memberEntity.groupIds.size() == 0)
						throw new APIException(
								Status.ERROR_PRECONDITION_FAILED,
								"A member should be assigned to atleast 1 group");
				}

				if (change)
					ofy(namespace).save().entity(memberEntity).now();

				return memberEntity.toProp();
			}
		});

		return memberProp;
	}

	static MemberEntity populateContactDetails(MemberEntity memberEntity,
			ContactDetailProp contact) {
		if (contact.email != null) {
			Utils.ensureValidEmail(contact.email);
			memberEntity.email = contact.email.toLowerCase();
		}

		if (contact.firstName != null)
			memberEntity.firstName = contact.firstName;

		if (contact.lastName != null)
			memberEntity.lastName = contact.lastName;

		if (contact.homeAddress != null)
			memberEntity.homeAddress = contact.homeAddress;

		if (contact.officeAddress != null)
			memberEntity.officeAddress = contact.officeAddress;

		if (contact.homePhone != null) {
			Utils.ensureValidPhoneNumber(contact.homePhone);
			memberEntity.homePhone = contact.homePhone;
		}

		if (contact.officePhone != null) {
			Utils.ensureValidPhoneNumber(contact.officePhone);
			memberEntity.officePhone = contact.officePhone;
		}

		if (contact.mobilePhone != null) {
			Utils.ensureValidPhoneNumber(contact.mobilePhone);
			memberEntity.mobilePhone = contact.mobilePhone;
		}

		return memberEntity;
	}

	static void populateDependantFields(String client, MemberEntity memberEntity) {
		// this is just a convenience method, it calls the underlying array
		// method
		List<MemberEntity> memberEntities = new ArrayList<>();
		memberEntities.add(memberEntity);

		populateDependantFields(client, memberEntities);
	}

	static void populateDependantFields(String client,
			List<MemberEntity> memberEntities) {

		for (MemberEntity memberEntity : memberEntities) {
			populateDependantFieldsWithoutDBQuery(memberEntity);
		}

		// fields that involve database queries
		List<Long> programIds = new ArrayList<>();
		for (MemberEntity memberEntity : memberEntities) {
			programIds.addAll(memberEntity.programIds);
		}

		Map<Long, ProgramProp> programProps = Program.get(client, programIds);

		for (MemberEntity memberEntity : memberEntities) {
			memberEntity.programTypeIds.clear();
			memberEntity.practiceIds.clear();

			for (long programId : memberEntity.programIds) {
				if (programProps.containsKey(programId)) {
					ProgramProp programProp = programProps.get(programId);

					memberEntity.programTypeIds
							.add(programProp.programTypeProp.programTypeId);
				}
			}

			List<UnverifiedProgramProp> uvprops = UnverifiedProgram
					.getUnverifiedProgramPropsFromJson(memberEntity.listUnverfiedProgramPropJson);

			if (uvprops != null) {
				for (UnverifiedProgramProp unverifiedProgramProp : uvprops) {
					memberEntity.programTypeIds
							.add(unverifiedProgramProp.programTypeId);
				}
			}

			memberEntity.practiceIds = ProgramType.getPracticeIds(client,
					memberEntity.programTypeIds);
		}
	}

	static MemberEntity populateDependantFieldsWithoutDBQuery(
			MemberEntity memberEntity) {
		// nameFirstChar
		if (memberEntity.firstName != null) {
			memberEntity.name = memberEntity.firstName.toLowerCase();
		} else if (memberEntity.email != null)
			memberEntity.name = memberEntity.email.toLowerCase();
		else if (memberEntity.mobilePhone != null)
			memberEntity.name = memberEntity.mobilePhone.toLowerCase();
		else if (memberEntity.homePhone != null)
			memberEntity.name = memberEntity.homePhone.toLowerCase();
		else if (memberEntity.officePhone != null)
			memberEntity.name = memberEntity.officePhone.toLowerCase();
		else {
			// should never come here as either email or a valid phone number
			// should be specified
			throw new APIException(
					Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED,
					"Either email or a valid phone number should be specified when adding or updating member");
		}
		memberEntity.nameFirstChar = memberEntity.name.substring(0, 1);

		memberEntity.qsTags = Utils.getQSTags(memberEntity.email,
				memberEntity.firstName, memberEntity.lastName,
				memberEntity.homePhone, memberEntity.officePhone,
				memberEntity.mobilePhone);

		// phone numbers
		// HashSet<String> phoneNumbers = new HashSet<>();
		// if (memberEntity.mobilePhone != null)
		// phoneNumbers.add(memberEntity.mobilePhone);
		// if (memberEntity.officePhone != null)
		// phoneNumbers.add(memberEntity.officePhone);
		// if (memberEntity.homePhone != null)
		// phoneNumbers.add(memberEntity.homePhone);
		//
		// memberEntity.phoneNumbers = phoneNumbers;

		return memberEntity;
	}

	public static MemberEntity safeGet(String namespace, long memberId) {

		MemberEntity entity = ofy(namespace).load().type(MemberEntity.class)
				.id(memberId).get();

		if (null == entity)
			throw new APIException(Status.ERROR_RESOURCE_DOES_NOT_EXIST,
					"There is no member with id [" + memberId + "]");

		return entity;
	}

	static List<MemberProp> query(String namespace, String searchString,
			Set<Long> groupIds, Set<Long> practiceIds, Integer maxResultSize) {

		Query<MemberEntity> q = ofy(namespace).load().type(MemberEntity.class);

		if ((null != groupIds) && (groupIds.size() != 0))
			q = q.filter("groupIds in", groupIds);

		if ((null != practiceIds) && (practiceIds.size() != 0))
			q = q.filter("practiceIds in", practiceIds);

		if (null != searchString) {
			if (searchString.length() < 3)
				Utils.throwIncorrectSpecException("Search string when specified should be atleast 3 chars");
			searchString = searchString.toLowerCase();
			Set<String> searchTags = Utils.getQSTags(searchString);
			for (String tag : searchTags) {
				q = q.filter("qsTags", tag);
			}
		}

		List<MemberEntity> entities = q.list();

		List<MemberProp> props = new ArrayList<>();
		for (MemberEntity entity : entities)
			props.add(entity.toProp());

		Collections.sort(props);

		if ((null != maxResultSize) && (maxResultSize > 0))
			if (props.size() > maxResultSize)
				props = props.subList(0, maxResultSize);

		return props;
	}

	static List<MemberProp> getAllForGroup(String namespace, long groupId) {

		Query<MemberEntity> q = ofy(namespace).load().type(MemberEntity.class)
				.filter("groupIds", groupId);

		List<MemberEntity> entities = q.list();
		List<MemberProp> props = new ArrayList<>();
		for (MemberEntity entity : entities)
			props.add(entity.toProp());

		Collections.sort(props);
		return props;
	}
}